<!DOCTYPE html>
<html>

<head>
    <meta name="description" content="[a single mvvm]">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>111</title>
</head>

<body>
    <div id="root">
        <div>
            <!-- <div data-model="user"></div>
            <h1 data-model="title"></h1> -->
            <ul data-list="todos">
                <li data-list-item="todos">
                    <p data-class="todos:done" class="a" data-model="todos:creator"></p>
                    <p data-model="todos:date"></p>
                    <p data-model="todos:content"></p>
                    <ul data-list="todos:members">
                        <li data-list-item="todos:members">
                            <span data-model="todos:members:name"></span>
                        </li>
                    </ul>
                </li>
            </ul>
        </div>
    </div>
    <!-- <div id="bind">
        <h1 data-model="title"></h1>
        <input data-model="content" data-event="input" type="text" />
        <p data-model="content"></p>
        <p data-model="content"></p>
    </div>
    <div id="nest">
        <h1 data-model="title"></h1>
        <h2 data-model="todos:title"></h2>
        <ul data-list="todos:tag:members">
            <li data-list-item="todos:tag:members">
                <span data-model="todos:tag:members:name"></span>
            </li>
        </ul>
    </div>
    <div id="arr">
        <div>
            <div>
                <ul data-list="todos:words:arr">
                    <li data-list-item="todos:words:arr">
                        <div>
                            <div>
                                <span data-model="todos:words:arr"></span>
                            </div>
                        </div>
                    </li>
                </ul>
            </div>
        </div>
    </div> -->
    <script>
        class Register {
            constructor() {
                this.routes = []
            }

            regist(obj, k, fn) {
                const _i = this.routes.find(function (el) {
                    if ((el.key === k || el.key.toString() === k.toString()) && Object.is(el.obj, obj)) {
                        return el
                    }
                })
                if (_i) {
                    _i.fn.push(fn)
                } else {
                    this.routes.push({
                        obj: obj,
                        key: k,
                        fn: [fn]
                    })
                }
            }

            build() {
                this.routes.forEach((route) => {
                    observer(route.obj, route.key, route.fn)
                })
            }
        }

        class Parser {
            constructor(el, data, elist) {
                this._data = data
                this.$register = new Register()
                this.$el = document.querySelector(el)
                this.$elist = elist
                this.$frag = this.node2Fragment(this.$el)
                this.scan(this.$frag)
                this.$el.appendChild(this.$frag)
                this.$register.build()
            }

            node2Fragment(el) {
                const fragment = document.createDocumentFragment()
                let child = el.firstChild
                while (child) {
                    fragment.appendChild(child)
                    child = el.firstChild
                }
                return fragment
            }

            scan(node) {
                if (node === this.$frag || !node.getAttribute('data-list')) {
                    for (let i = 0; i < node.children.length; i++) {
                        const _thisNode = node.children[i]
                        if (node.path) {
                            _thisNode.path = node.path
                        }
                        this.parseEvent(_thisNode)
                        this.parseClass(_thisNode)
                        this.parseModel(_thisNode)
                        if (_thisNode.children.length) {
                            this.scan(_thisNode)
                        }
                    }
                } else {
                    this.parseList(node)
                }
            }

            parseData(str, node) {
                const _list = str.split(':')
                let _data,
                    _path,
                    _j = 1
                let p = []
                _list.forEach((key, index) => {
                    if (index === 0) {
                        _data = this._data[key]
                        p.push(key)
                    } else {
                        if (node.path) {
                            _path = node.path[_j++]
                            if (_path === key) {
                                _data = _data[key]
                            } else {
                                p.push(_path)
                                _data = _data[_path][key]
                                _j++
                            }
                        } else {
                            _data = _data[key]
                        }
                        p.push(key)
                    }
                })
                if (node.path && node.path.length > p.length) {
                    const _i = node.path[node.path.length - 1]
                    if (typeof _i !== 'number') {
                        return
                    }
                    _data = _data[_i]
                    p.push(_i)
                }
                if (!node.path || node.path !== p) {
                    node.path = p
                }
                return {
                    path: p,
                    data: _data
                }
            }

            parseEvent(node) {
                if (node.getAttribute('data-event')) {
                    const eventName = node.getAttribute('data-event')
                    const _type = this.$elist[eventName].type
                    const _fn = this.$elist[eventName].fn.bind(node)
                    if (_type === 'input') {
                        let cmp = false
                        node.addEventListener('compositionstart', function () {
                            cmp = true
                        })
                        node.addEventListener('compositionend', function () {
                            cmp = false
                            node.dispatchEvent(new Event('input'))
                        })
                        node.addEventListener('input', function () {
                            if (!cmp) {
                                let start = this.selectionStart
                                let end = this.selectionEnd
                                _fn()
                                this.setSelectionRange(start, end)
                            }
                        })
                    } else {
                        node.addEventListener(_type, _fn)
                    }
                }
            }

            parseClass(node) {
                if (node.getAttribute('data-class')) {
                    const className = node.getAttribute('data-class')
                    const _data = this.parseData(className, node)
                    if (!node.classList.contains(_data.data)) {
                        node.classList.add(_data.data)
                    }
                    this.$register.regist(this._data, _data.path, function (old, now) {
                        node.classList.remove(old)
                        node.classList.add(now)
                    })
                }
            }

            parseModel(node) {
                if (node.getAttribute('data-model')) {
                    const modelName = node.getAttribute('data-model')
                    const _data = this.parseData(modelName, node)
                    if (node.tagName === 'INPUT') {
                        node.value = _data.data
                    } else {
                        node.innerText = _data.data
                    }
                    this.$register.regist(this._data, _data.path, function (old, now) {
                        if (node.tagName === 'INPUT') {
                            node.value = now
                        } else {
                            node.innerText = now
                        }
                    })
                }
            }

            parseList(node) {
                const _item = this.parseListItem(node)
                const _list = node.getAttribute('data-list')
                const _listData = this.parseData(_list, node)
                _listData.data.forEach((_dataItem, index) => {
                    const _copyItem = _item.cloneNode(true)
                    if (node.path) {
                        _copyItem.path = node.path.slice()
                    }
                    if (!_copyItem.path) {
                        _copyItem.path = []
                    }
                    _copyItem.path.push(index)
                    this.scan(_copyItem)
                    node.insertBefore(_copyItem, _item)
                })
                node.removeChild(_item)
                this.$register.regist(this._data, _listData.path, () => {
                    while (node.firstChild) {
                        node.removeChild(node.firstChild)
                    }
                    const _listData = this.parseData(_list, node)
                    node.appendChild(_item)
                    _listData.data.forEach((_dataItem, index) => {
                        const _copyItem = _item.cloneNode(true)
                        if (node.path) {
                            _copyItem.path = node.path.slice()
                        }
                        if (!_copyItem.path) {
                            _copyItem.path = []
                        }
                        _copyItem.path.push(index)
                        this.scan(_copyItem)
                        node.insertBefore(_copyItem, _item)
                    })
                    node.removeChild(_item)
                })
            }

            parseListItem(node) {
                // node => ul
                const me = this
                let target
                !function getItem(node) {
                    for (let i = 0; i < node.children.length; i++) {
                        const _thisNode = node.children[i]
                        if (node.path) {
                            _thisNode.path = node.path.slice()
                        }
                        me.parseEvent(_thisNode)
                        me.parseClass(_thisNode)
                        me.parseModel(_thisNode)
                        if (_thisNode.getAttribute('data-list-item')) {
                            target = _thisNode
                        } else {
                            getItem(_thisNode)
                        }
                    }
                }(node)
                return target
            }
        }

        function observer(obj, k, callback) {
            if (Object.prototype.toString.call(k) === '[object Array]') {
                observePath(obj, k, callback)
            } else {
                let old = obj[k]
                if (Object.prototype.toString.call(old) === '[object Array]') {
                    observeArray(old, callback)
                } else if (old.toString() === '[object Object]') {
                    observeAllKey(old, callback)
                } else {
                    Object.defineProperty(obj, k, {
                        enumerable: true,
                        configurable: true,
                        get: function () {
                            return old
                        },
                        set: function (now) {
                            if (now !== old) {
                                callback.forEach((fn) => {
                                    fn(old, now)
                                })
                            }
                            old = now
                        }
                    })
                }
            }
        }

        function observePath(obj, path, callback) {
            let _path = obj
            let _key
            path.forEach((p, index) => {
                if (parseInt(p) === p) {
                    p = parseInt(p)
                }
                if (index < path.length - 1) {
                    _path = _path[p]
                } else {
                    _key = p
                }
            })
            observer(_path, _key, callback)
        }

        function observeArray(arr, callback) {
            const oam = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
            const arrayProto = Array.prototype
            const hackProto = Object.create(Array.prototype)
            oam.forEach(function (method) {
                Object.defineProperty(hackProto, method, {
                    writable: true,
                    enumerable: true,
                    configurable: true,
                    value: function (...arg) {
                        let old = arr.slice()
                        let now = arrayProto[method].call(this, ...arg)
                        callback.forEach((fn) => {
                            fn(old, this, ...arg)
                        })
                        return now
                    }, 
                })
            })
            arr.__proto__ = hackProto
        }

        function observeAllKey(obj, callback) {
            Object.keys(obj).forEach(function (key) {
                observer(obj, key, callback)
            })
        }

        //to use:
        let data = {
            // title: '标题',
            // user: 'korey',
            todos: [
                {
                    creator: 'qxq',
                    done: 'undone',
                    content: '吃饭',
                    date: '2016-11-17',
                    members: [
                        {
                            name: 'qxq1'
                        },
                        {
                            name: 'qxq2'
                        },
                        {
                            name: 'qxq3'
                        }
                    ]
                },
                {
                    creator: 'tr',
                    done: 'undone',
                    content: '睡觉',
                    date: '2016-12-17',
                    members: [
                        {
                            name: 'tr1'
                        },
                        {
                            name: 'tr2'
                        }
                    ]
                }
            ]
        }
        new Parser('#root', data)
        data.todos[0].done = 'done'
        data.todos[1].members[0].name = 'UU'
        data.todos[1].members.push({ name: 'hitori' })
        // data.title = 'todo list!'
        data.todos.push({
            creator: 'k',
            done: 'undone',
            date: '2016-12-24',
            content: '打豆豆',
            members: [
                {
                    name: 'k1'
                },
            ]
        })
        data.todos[2].members.push({
            name: 'k2'
        })
        data.todos.splice(1, 1)

        // let bind = {
        //     title: 'data bind',
        //     content: 'qaq'
        // }
        // let e = {
        //     'input': {
        //         type: 'input',
        //         fn: function (event) {
        //             bind.content = this.value
        //             console.log(bind.content)
        //         }
        //     }
        // }
        // new Parser('#bind', bind, e)
    </script>
</body>

</html>